package com.aabte.commons.jwt;

import com.aabte.commons.encrypt.AbstractEncryptor;
import com.aabte.commons.encrypt.EncryptException;
import com.aabte.commons.encrypt.rsa.EncryptorRSAFactory;
import com.aabte.commons.encrypt.rsa.RSAPrivateKeyEncryptor;
import com.aabte.commons.encrypt.rsa.RSAPublicKeyEncryptor;
import com.aabte.commons.json.JSONUtils;
import com.aabte.commons.jwt.exception.TokenCreateException;
import com.aabte.commons.jwt.exception.TokenException;
import com.aabte.commons.jwt.exception.TokenExpiredException;
import com.aabte.commons.jwt.exception.TokenParseException;
import com.aabte.commons.util.ReflectionUtils;
import io.jsonwebtoken.*;
import io.jsonwebtoken.impl.DefaultClaims;
import lombok.extern.slf4j.Slf4j;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.time.Instant;
import java.util.*;

/**
 * @author Daniel
 * @version 1.0
 * @date 2020/4/5
 */
@Slf4j
public class JWTTokenUtils {

  private static final JWTBase64Decoder DECODER = JWTBase64Decoder.getInstance();
  private static final JWTBase64Encoder ENCODER = JWTBase64Encoder.getInstance();

  private static final String DEFAULT_CONFIG_PATH = "jwt";
  private static final String DEFAULT_CONFIG_FILE_NAME = "jwt.properties";

  /**
   * 环境变量名，通过此变量指定相关配置文件所在目录
   */
  private static final String JWT_CONFIG_PATH = "JWT_CONFIG_PATH";

  /**
   * 配置项：签发者
   */
  private static final String ISSUES_PROPERTY = "jwt.issuer";
  /**
   * 配置项：过期时间
   */
  private static final String EXPIRE_TIME_PROPERTY = "jwt.expire-time-second";

  /**
   * 配置项：刷新周期
   */
  private static final String REFRESH_PERIOD_PROPERTY = "jwt.refresh-period-second";

  private static final int DEFAULT_EXPIRE_TIME = 24 * 60 * 60;
  private static final int DEFAULT_REFRESH_PERIOD = DEFAULT_EXPIRE_TIME / 2;

  /**
   * 配置文件目录
   */
  private static String configPath = DEFAULT_CONFIG_PATH;

  /**
   * 过期时间
   */
  private static int expireTimeSecond = DEFAULT_EXPIRE_TIME;

  /**
   * token刷新周期
   */
  private static int refreshPeriodSecond = DEFAULT_REFRESH_PERIOD;

  /**
   * 配置
   */
  private static final Properties PROPERTIES = new Properties();

  private static final RSAPrivateKeyEncryptor RSA_PRIVATE_KEY_ENCRYPTOR =
      EncryptorRSAFactory.getPrivateEncryptor();

  private static final RSAPublicKeyEncryptor RSA_PUBLIC_KEY_ENCRYPTOR =
      EncryptorRSAFactory.getPublicEncryptor();

  static {
    loadProp();
    init();
  }

  public static Claims createClaims() {
    Date expiration = generateExpiration();
    Claims claims = new DefaultClaims();
    claims.setExpiration(expiration);
    claims.setIssuedAt(new Date());
    claims.setId(UUID.randomUUID().toString().replace("-", ""));
    return claims;
  }

  public static Claims createClaims(String key, Object tokenPayload) {
    Claims claims = createClaims();
    claims.put(key, tokenPayload);
    return claims;
  }

  public static Map<String, Object> createClaimsMap(String key, Object tokenPayload) {
    Map<String, Object> claims = createClaimsMap();
    claims.put(key, tokenPayload);
    return claims;
  }

  public static Map<String, Object> createClaimsMap() {
    Map<String, Object> claims = new HashMap<>(8);
    claims.put(Claims.EXPIRATION, generateExpiration());
    claims.put(Claims.ISSUER, PROPERTIES.getProperty(ISSUES_PROPERTY));
    claims.put(Claims.ISSUED_AT, new Date());
    claims.put(Claims.ID, UUID.randomUUID().toString().replace("-", ""));
    return claims;
  }

  public static String generateToken(Map<String, Object> claims) {
    return generateToken(claims, null);
  }

  public static String generateToken(Object obj) {
    Map<String, Object> claims;
    try {
      claims = ReflectionUtils.toMap(obj);
    } catch (Exception e) {
      throw new TokenCreateException("obj cannot be converted to Map type");
    }
    return generateToken(claims, null);
  }

  public static String generateToken(Claims claims) {

    if (null == claims.getExpiration()) {
      claims.setExpiration(generateExpiration());
    }

    return Jwts.builder()
        .setClaims(claims)
        .setIssuedAt(new Date())
        .setIssuer(PROPERTIES.getProperty(ISSUES_PROPERTY))
        .signWith(RSA_PRIVATE_KEY_ENCRYPTOR.key())
        .base64UrlEncodeWith(ENCODER)
        .compact();
  }

  public static String generateToken(Object obj, Date exp) {
    Map<String, Object> claims;
    try {
      claims = ReflectionUtils.toMap(obj);
    } catch (Exception e) {
      throw new TokenCreateException("obj cannot be converted to Map type");
    }
    return generateToken(claims, exp);
  }

  public static String generateToken(Map<String, Object> claims, Date exp) {
    JwtBuilder jwtBuilder =
        Jwts.builder()
            .setClaims(claims)
            .setIssuedAt(new Date())
            .setIssuer(PROPERTIES.getProperty(ISSUES_PROPERTY))
            .signWith(RSA_PRIVATE_KEY_ENCRYPTOR.key())
            .base64UrlEncodeWith(ENCODER);
    if (null == exp) {
      if (!claims.containsKey(Claims.EXPIRATION)) {
        jwtBuilder.setExpiration(generateExpiration());
      }
    }
    return jwtBuilder.compact();
  }

  public static long parseTokenIssuedAt(String token) throws TokenException {
    Claims claims = parseTokenBody2Claims(token);
    Date issuedAt = claims.getIssuedAt();
    return issuedAt.getTime();
  }

  public static <T> T parseTokenBody(String token, Class<T> clazz) throws TokenException {
    String body = parseTokenBody(token);
    return JSONUtils.fromJson(JSONUtils.toJson(body), clazz);
  }

  public static String parseTokenBody(String token) throws TokenException {
    Jws<String> stringJws;
    try {
      stringJws =
          Jwts.parserBuilder()
              .setSigningKey(RSA_PUBLIC_KEY_ENCRYPTOR.key())
              .base64UrlDecodeWith(DECODER)
              .build()
              .parsePlaintextJws(token);
    } catch (ExpiredJwtException e) {
      throw new TokenExpiredException("Token expired", e);
    } catch (Exception e) {
      throw new TokenParseException("Token incorrect", e);
    }
    return stringJws.getBody();
  }

  public static Claims parseTokenBody2Claims(String token) throws TokenException {
    Jws<Claims> claimsJws = parseToken2Jws(token);
    return claimsJws.getBody();
  }

  public static Jws<Claims> parseToken2Jws(String token) throws TokenException {
    try {
      return Jwts.parserBuilder()
          .setSigningKey(RSA_PUBLIC_KEY_ENCRYPTOR.key())
          .base64UrlDecodeWith(DECODER)
          .build()
          .parseClaimsJws(token);
    } catch (ExpiredJwtException e) {
      throw new TokenExpiredException("Token expired", e);
    } catch (Exception e) {
      throw new TokenParseException("Token incorrect", e);
    }
  }

  public static String tokenRefresh(String token) {
    Jws<Claims> claimsJws = parseToken2Jws(token);
    return tokenRefresh(claimsJws.getBody());
  }

  public static String tokenRefresh(Claims body) {
    body.setExpiration(generateExpiration());
    body.setIssuedAt(new Date());
    return generateToken(body);
  }

  public static Optional<String> tokenRefreshIfNeed(String token) {
    return tokenRefreshIfNeed(token, refreshPeriodSecond);
  }

  public static Optional<String> tokenRefreshIfNeed(String token, int refreshPeriod) {
    Jws<Claims> claimsJws = parseToken2Jws(token);
    Claims body = claimsJws.getBody();
    long issuedAt = body.getIssuedAt().getTime();
    long expiration = body.getExpiration().getTime();

    long currentMilli = Instant.now().toEpochMilli();
    if (expiration - currentMilli >= 0) {
      throw new TokenExpiredException();
    }

    if (currentMilli - issuedAt >= refreshPeriod * 1000) {
      return Optional.of(tokenRefresh(body));
    }
    return Optional.empty();
  }

  public static Date generateExpiration() {
    long expirationTimeMillis = generateExpirationTimeMillis();
    return new Date(expirationTimeMillis);
  }

  /**
   * 生成token时间 = 当前时间 + expiration（properties中配置的失效时间）
   */
  private static long generateExpirationTimeMillis() {
    return Instant.now().toEpochMilli() + expireTimeSecond * 1000;
  }

  private static void init() {
    String property = PROPERTIES.getProperty(EXPIRE_TIME_PROPERTY);
    try {
      expireTimeSecond = Integer.parseInt(property);
    } catch (NumberFormatException e) {
      log.warn("configuration item {} error. value: {}", EXPIRE_TIME_PROPERTY, property);
    }

    property = PROPERTIES.getProperty(REFRESH_PERIOD_PROPERTY);
    try {
      refreshPeriodSecond = Integer.parseInt(property);
    } catch (NumberFormatException e) {
      log.warn("configuration item {} error. value: {}", REFRESH_PERIOD_PROPERTY, property);
    }
  }

  private static void loadProp() {
    String value = System.getenv(JWT_CONFIG_PATH);
    if (null != value && value.trim().length() > 0) {
      File file = new File(value, DEFAULT_CONFIG_FILE_NAME);
      try {
        loadPropertiesFile(file);
        configPath = value.trim();
        return;
      } catch (Exception e) {
        // nothing
      }
    }

    File file = new File(DEFAULT_CONFIG_PATH, DEFAULT_CONFIG_FILE_NAME);
    try {
      loadPropertiesFile(file);
    } catch (Exception e) {
      log.warn("load encrypt configuration file failed.");
    }
  }

  private static void loadPropertiesFile(File file) {
    URL resource = AbstractEncryptor.class.getClassLoader().getResource("");
    if (null != resource) {
      String classpath = resource.getPath();
      file = new File(classpath, file.getPath());
      if (file.isFile() && file.canRead()) {
        try (InputStream in = new FileInputStream(file)) {
          PROPERTIES.load(in);
        } catch (IOException e) {
          throw new EncryptException(e);
        }
      } else {
        throw new EncryptException("read encrypt configuration file failed.");
      }
    }
  }
}
